iT邦幫忙

2023 iThome 鐵人賽

DAY 30
0

Applicative Fuctor 定律

為求說明,以下回顧 Applicative Functor 的方法宣告會將所有參數完整呈現,故會與 Day 28 - Applicative Functors (1) 看到的有所不同。

Left 和 Right Identity

因為 Applicative Functor 也是繼承至 Functor 介面,所以它也是遵守著在 Day 25 - Functors 提到的 Functor 定律,

map(x)(a => a) == x

這隱含著其它 Applicative Functor 會用到的定律,如同我們用 map2 和 unit 來實作 map 方法那般,

為求說明,此 map 宣告會將所有參數完整呈現,故會與 Day 28 看到的有所不同。

def map[B](fa: F[A])(f: A => B): F[B] =
  map2(fa, unit(()))((a, _) => f(a))

我們也可以把 unit 放到左邊,

def map[B](fa: F[A])(f: A => B): F[B] =
  map2(unit(()), fa)((_, a) => f(a))

若把 unit 跟 fa:F[A] 當做參數丟給 map2,不管怎麼擺,其 map2 結果都是 fa,這也被稱為 Left 和 Right identity。

map2(unit(()), fa)((_, a) => a) == fa // Left identity
map2(fa, unit(()))((a, _) => a) == fa // Right identity

結合律 (Associativity Law)

首先來看一下 Day 28 的 map3 宣告,我們在 Exercise D28-2 解答中使用了 apply 和 unit 來實現,

def map3[A, B, C, D](fa: F[A], fb: F[B], fc: F[C])(f: (A, B, C) => D): F[D] = 
	apply(apply(apply(unit(f.curried))(fa))(fb))(fc)

但如果我們用 map2 的觀點來看,這裡可以先合併 fa 和 fb, 然後在將其結果與 fc 合併,或者 fb 和 fc 先合併也可以,這種似曾相識感覺其實就跟 Monoid 和 Monad 的結合律一樣,

combine(a, combine(b, c)) == combine(combine(a, b), c)
compose(f, op(g, h)) == compose(compose(f, g), h)

這裡我們可以用 product 來表述結合律,回憶一下 Exercise D28-1 的解答中我們使用了 map2 來實現 product,

def product[B](fa: F[A], fb: F[B]): F[(A, B)] =
  map2(fa, fb)((_, _))

然後若我們要從右邊開始配對,我們當然也能把它改成從左邊開始配對,

def assoc[A,B,C](p: (A, (B, C))): ((A, B), C) =
  p match { case (a, (b, c)) => ((a, b), c) 

最後用 product 來表述 Applicative Fuctor 結合律的結果如下。

product(product(fa, fb),fc) == map(product(fa, product(fb, fc)))(assoc)

自然轉換 (Natural transformation)

最後一個定律是 自然轉換,看一下以下例子,我們用 map2 來產出結果,

val F: Applicative[Option] = ...

case class Employee(name: String, id: Int)
case class Pay(rate: Double, hoursPerMonth: Double)

def format(employee: Option[Employee], pay: Option[Pay]): Option[String] =
  F.map2(employee, pay)((e, p) =>
    s"${e.name} 賺了 ${p.rate * p.hoursPerMonth} 元"
  )

val employee: Option[Employee] = ...
val pay: Option[Pay] = ...
format(employee, pay)

這個 format 方法其實並不需要知道 Employee 或 Pay 的細節,所以 format 方法可以重構成只接收 String 和 Double 做為參數,然後 使用 map 方法取得 name 和 pay,

def format(name: Option[String], pay: Option[Double]): Option[String] =
  F.map2(name, pay)((n, p) =>
    s"$n 賺了 $p 元"
  )

val employee: Option[Employee] = ...
val pay: Option[Pay] = ...

format(
  F.map(employee)(_.name), 
  F.map(pay)(p => p.rate * p.hoursPerMonth)
)

這個簡單、直覺、我們常在用的方法,用正式一點方式表述的話就是自然轉換定律,

map2(a, b)(productF(f, g)) == product(map(a)(f), map(b)(g))

productF 能合併 2 個方法為 1 個方法,其方法宣告如下。

def productF[I, O, I2, O2](f: I => O, g: I2 => O2): (I, I2) => (O, O2) =
  (i, i2) => (f(i), g(i2))

總結

Applicative Functors 是一個功能沒有 Monad 這麼強,但比較泛用的抽象介面,我們可以自由選擇要用 map2 或 apply 做為 Applicative Functor 的核心方法,它們都有能力將方法提升至更高的層級,讓我們有更多組合器方法可以使用,Monoid、Monad、Functor 和 Applicative Functor 的定律都大同小異,抽象出這些介面的重點是訓練我們能夠辨識出模式,然後用 functional programming 風格來實作 library。


上一篇
Applicative Functors (2)
系列文
用 Scala 3 寫的 Functional Programming 會長什麼樣子?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言